/*
 * Class
 * PascalDistribution
 */

package pl.abstractvoid.distributions;

import java.util.Random;
import java.util.TreeMap;
import pl.abstractvoid.datamodel.parameters.ParameterSet;
import pl.abstractvoid.datamodel.parameters.SingleParameter;
import pl.abstractvoid.datamodel.parameters.exceptions.NoSuchParameterException;
import pl.abstractvoid.distributions.exceptions.ParameterValidationFailException;
import pl.abstractvoid.rconnector.RCallerInstance;
import pl.abstractvoid.rconnector.RInterpreter;
import rcaller.exception.ParseException;

/**
 * Represents Pascal (negative binomial) probability distribution.
 * 
 * @author Wojciech Szałapski
 */
public class PascalDistribution extends AbstractDistribution {    
    
    /**
     * String representing the name of the variable which stores the number of
     * successes in R.
     */
    private final static String successesString = "successes";
    /**
     * String representing the name of the variable which stores the probability
     * of a success in a trial in R.
     */
    private final static String successProbabilityString = "successProbability";
    /**
     * Names for output values used by R interpreter.
     */
    private final static String[] outputTableNames;
    /**
     * Names for output values used by user interface.
     */
    private final static String[] outputTableVisibleNames;
    /**
     * Buffer for the script executed when the output is computed.
     */
    private final static StringBuilder pascalSetUpOutputScript = new StringBuilder();
    /**
     * Buffer for the script executed when the cumulative distribution plot is
     * generated.
     */
    private final static StringBuilder pascalSetUpCumulativeDistributionPlotScript = new StringBuilder();
    /**
     * Buffer for the script executed when the probability density/mass function 
     * plot is generated.
     */
    private final static StringBuilder pascalSetUpProbabilityPlotScript = new StringBuilder();
    
    /*
     * Initialization of:
     *  - Names of output parameters.
     *  - Script for computing output parameters.
     *  - Scripts for generating plots.
     */
    static {
        final int numberOfParameters = 3;
        outputTableNames = new String[numberOfParameters];
        outputTableVisibleNames = new String[numberOfParameters];
        outputTableNames[0] = "expectedValue";
        outputTableNames[1] = "variance";
        outputTableNames[2] = "standardDeviation";
        
        pascalSetUpOutputScript.append(outputTableNames[0]).append(" = ").
                append(RInterpreter.inputRList).append("$").append(successesString).append(" * (1 - ").
                append(RInterpreter.inputRList).append("$").append(successProbabilityString).append(") / ").
                append(RInterpreter.inputRList).append("$").append(successProbabilityString).append("\n");
        pascalSetUpOutputScript.append(outputTableNames[1]).append(" = ").
                append(outputTableNames[0]).append(" / ").
                append(RInterpreter.inputRList).append("$").append(successProbabilityString).append("\n");
        pascalSetUpOutputScript.append(outputTableNames[2]).append(" = ").
                append(outputTableNames[1]).append("^(1/2)\n");
        pascalSetUpOutputScript.append(RInterpreter.outputRList).append(" = list(");
        String prefix = "";
        for (String name : outputTableNames) {
            pascalSetUpOutputScript.append(prefix).append(name).append(" = ").append(name);
            prefix = ", ";
        }
        pascalSetUpOutputScript.append(")");
        
        pascalSetUpCumulativeDistributionPlotScript.append("plotX = c(0:(1 + qnbinom(0.9999, ").
                append(RInterpreter.inputRList).append("$").append(successesString).append(", ").
                append(RInterpreter.inputRList).append("$").append(successProbabilityString).
                append(")))\n").append("normalPlot(plotX, pnbinom(plotX, ").
                append(RInterpreter.inputRList).append("$").append(successesString).append(", ").
                append(RInterpreter.inputRList).append("$").append(successProbabilityString).append("), "
                + "type = 's')");
        
        pascalSetUpProbabilityPlotScript.append(pascalSetUpCumulativeDistributionPlotScript.
                toString().replaceAll("pnbinom", "dnbinom").replaceAll("type = 's'", "type = 'p'"));
    }

    /**
     * Constructs the distribution using given input parameters.
     * 
     * @param successes Number of successes.
     * @param successProbability Probability of success in each trial.
     * @throws ParameterValidationFailException
     * @throws NoSuchParameterException 
     */
    public PascalDistribution(int successes, double successProbability)
            throws ParameterValidationFailException, NoSuchParameterException {
        super(pascalSetUpOutputScript, pascalSetUpCumulativeDistributionPlotScript, 
              pascalSetUpProbabilityPlotScript);
        distributionName = "pascalDistribution";
        rState = new RCallerInstance();
        SingleParameter successesParam = new SingleParameter(successesString);
        successesParam.setParameterValue(successes);
        successesParam.setInteger();
        SingleParameter successProbabilityParam = new SingleParameter(successProbabilityString);
        successProbabilityParam.setParameterValue(successProbability);
        TreeMap<String, SingleParameter> singleParameters = new TreeMap<>();
        singleParameters.put(successesString, successesParam);
        singleParameters.put(successProbabilityString, successProbabilityParam);
        ParameterSet parameterSet = new ParameterSet();
        parameterSet.setSingleParameters(singleParameters);
        updateInput(parameterSet);
    }

    @Override
    protected void validate(ParameterSet parameters) throws ParameterValidationFailException {
        TreeMap<String, SingleParameter> singleParameters = parameters.getSingleParameters();
        SingleParameter successesParam = singleParameters.get(successesString);
        if (successesParam.getParameterValue() < 0) {
            throw new ParameterValidationFailException("invalidSuccessesNumber");
        }
        SingleParameter successProbabilityParam = singleParameters.get(successProbabilityString);
        if (successProbabilityParam.getParameterValue() <= 0 || successProbabilityParam.getParameterValue() > 1) {
            throw new ParameterValidationFailException("invalidSuccessProbability");
        }
        if (successesParam.getParameterValue() / successProbabilityParam.getParameterValue() > 5e5) {
            throw new ParameterValidationFailException("tooComplexParameters");
        }
    }
    
    @Override
    public ParameterSet loadParameters() throws NoSuchParameterException, ParseException {
        rState.runCustomCode("", RInterpreter.inputRList);
        SingleParameter successesParam = new SingleParameter(successesString);
        successesParam.setParameterValue(getSingleParameterValue(successesString));
        successesParam.setInteger();
        SingleParameter successProbabilityParam = new SingleParameter(successProbabilityString);
        successProbabilityParam.setParameterValue(getSingleParameterValue(successProbabilityString));
        TreeMap<String, SingleParameter> singleParameters = new TreeMap<>();
        singleParameters.put(successesString, successesParam);
        singleParameters.put(successProbabilityString, successProbabilityParam);
        ParameterSet result = new ParameterSet();
        result.setSingleParameters(singleParameters);
        return result;
    }

    @Override
    protected void initializeOutputTable() {
        outputTable = new SingleParameter[outputTableNames.length];
        for (int i = 0; i < outputTableNames.length; ++i) {
            outputTable[i] = new SingleParameter(outputTableNames[i]);
        }
    }
    
    /**
     * Returns a new instance of a pascal distribution with random number of
     * successes (from 0 to 50) and random probability of success (from the range
     * [0.001, 1) ).
     * 
     * @return New instance of pascal distribution with random parameters.
     */
    public static PascalDistribution getSampleDistributionData() {
        Random generator = new Random();
        int successes = generator.nextInt(51);
        double successProbability = generator.nextDouble();
        if (successProbability == 0) {
            successProbability = 0.001;
        }
        try {
            return new PascalDistribution(successes, successProbability);
        } catch (ParameterValidationFailException | NoSuchParameterException ex) {
            return null;
        }
    }
}